<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <title>框架开发 - 为企业级框架和应用而生</title>
  <meta charset="utf-8">
  <meta name="description" content="index.description">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <link rel="icon" href="/images/favicon.png" type="image/x-icon">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.css" />
<link rel="stylesheet" href="/css/index.css">

    <script>
    !function(t,e,a,r,c){t.TracertCmdCache=t.TracertCmdCache||[],t[c]=window[c]||
      {_isInit:!0,call:function(){t.TracertCmdCache.push(arguments)},
      start:function(t){this.call('start',t)}},t[c].l=new Date;
      var n=e.createElement(a),s=e.getElementsByTagName(a)[0];
      n.async=!0,n.src=r,s.parentNode.insertBefore(n,s)}
    (window,document,'script','https://tracert.alipay.com/tracert.js','Tracert');
      Tracert.start({
        plugins: [ 'BucName' ],
        spmAPos: 'a454',
        spmBPos: 'b4893',
      });
    </script>
  
</head>
<body>
  <div class="nav" >
  <header>
    <a href="/zh-cn/" class="nav-logo leftpadding" alt="egg"><img src="https://zos.alipayobjects.com/rmsportal/VTcUYAaoKqXyHJbLAPyF.svg"></a>
    <ul class="nav-item">
      <li>
        <form id="search-form">
          <input type="text" id="search-query" class="search-query st-default-search-input">
        </form>
      </li>
      <li><a href="/zh-cn/intro/" alt="指南">指南</a></li><li><a href="/api/" alt="API">API</a></li><li><a href="/zh-cn/tutorials/index.html" alt="教程">教程</a></li><li><a href="https://github.com/search?q=topic%3Aegg-plugin&type=Repositories" alt="插件">插件</a></li><li><a href="https://github.com/eggjs/egg/releases" alt="发布日志">发布日志</a></li>
      
      
        <li class="translations">
          <a class="nav-link">Translations</a>
          <span class="arrow"></span><ul id="dropdownContent" class="dropdown-content"><li><a id="en" href="/en/advanced/framework.html" >English</a></li><li><a id="zh-cn" href="/zh-cn/advanced/framework.html" style="color: #22ab28">中文</a></li></ul>
        </li>
      
      <li><iframe src="https://ghbtns.com/github-btn.html?user=eggjs&repo=egg&type=star&count=true" frameborder="0" scrolling="0" width="150px" height="20px"></iframe></li>
    </ul>
    <a id="mobileTrigger" href="#" class="mobile-trigger">
      <ul>
        <li></li>
        <li></li>
        <li></li>
      </ul>
    </a>
  </header>
</div>
  <div id="container" class="container">
    <div class="page-main">
  <article class="markdown-body">
    <h1>框架开发</h1>
    <p>如果你的团队遇到过：</p>
<ul>
<li>维护很多个项目，每个项目都需要复制拷贝诸如 <code>gulpfile.js</code> / <code>webpack.config.js</code> 之类的文件。</li>
<li>每个项目都需要使用一些相同的类库，相同的配置。</li>
<li>在新项目中对上面的配置做了一个优化后，如何同步到其他项目？</li>
</ul>
<p>如果你的团队需要：</p>
<ul>
<li>统一的技术选型，比如数据库、模板、前端框架及各种中间件设施都需要选型，而框架封装后保证应用使用一套架构。</li>
<li>统一的默认配置，开源社区的配置可能不适用于公司，而又不希望应用去配置。</li>
<li>统一的部署方案，通过框架和平台的双向控制，应用只需要关注自己的代码，具体查看<a href="../core/deployment.html">应用部署</a></li>
<li>统一的代码风格，框架不仅仅解决代码重用问题，还可以对应用做一定约束，作为企业框架是很必要的。Egg 在 Koa 基础上做了很多约定，框架可以使用 <a href="./loader.html">Loader</a> 自己定义代码规则。</li>
</ul>
<p>为此，Egg 为团队架构师和技术负责人提供 <code>框架定制</code> 的能力，框架是一层抽象，可以基于 Egg 去封装上层框架，并且 Egg 支持多层继承。</p>
<p>这样，整个团队就可以遵循统一的方案，并且在项目中可以根据业务场景自行使用插件做差异化，当后者验证为最佳实践后，就能下沉到框架中，其他项目仅需简单的升级下框架的版本即可享受到。</p>
<p>具体可以参见<a href="../tutorials/progressive.html">渐进式开发</a>。</p>
<h2 id="框架与多进程"><a class="markdown-anchor" href="#框架与多进程">#</a> 框架与多进程</h2>
<p>框架的扩展是和多进程模型有关的，我们已经知道<a href="../core/cluster-and-ipc.html">多进程模型</a>，也知道 Agent Worker 和 App Worker 的区别，所以我们需要扩展的类也有两个 Agent 和 Application，而这两个类的 API 不一定相同。</p>
<p>在 Agent Worker 启动的时候会实例化 Agent，而在 App Worker 启动时会实例化 Application，这两个类又同时继承 <a href="https://github.com/eggjs/egg-core" target="_blank" rel="noopener">EggCore</a>。</p>
<p>EggCore 可以看做 Koa Application 的升级版，默认内置 <a href="./loader.html">Loader</a>、<a href="../basics/router.html">Router</a> 及应用异步启动等功能，可以看做是支持 Loader 的 Koa。</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">      Koa Application</span><br><span class="line">             ^</span><br><span class="line">          EggCore</span><br><span class="line">             ^</span><br><span class="line">      ┌──────┴───────┐</span><br><span class="line">      │              │</span><br><span class="line">  Egg Agent      Egg Application</span><br><span class="line">     ^               ^</span><br><span class="line">agent worker     app worker</span><br></pre></td></tr></table></figure>
<h2 id="如何定制一个框架"><a class="markdown-anchor" href="#如何定制一个框架">#</a> 如何定制一个框架</h2>
<p>你可以直接通过 [egg-init] 选择 <a href="https://github.com/eggjs/egg-boilerplate-framework" target="_blank" rel="noopener">framework</a> 脚手架来快速上手。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ egg-init --<span class="built_in">type</span>=framework yadan</span><br><span class="line">$ <span class="built_in">cd</span> yadan</span><br><span class="line">$ npm i</span><br><span class="line">$ npm <span class="built_in">test</span></span><br></pre></td></tr></table></figure>
<p>但同样，为了让大家了解细节，接下来我们还是手把手来定制一个框架，具体代码可以查看<a href="https://github.com/eggjs/examples/tree/master/framework" target="_blank" rel="noopener">示例</a></p>
<h3 id="框架-api"><a class="markdown-anchor" href="#框架-api">#</a> 框架 API</h3>
<p>Egg 框架提供了一些 API，所有继承的框架都需要提供，只增不减。这些 API 基本都有 Agent 和 Application 两份。</p>
<h4 id="eggstartcluster"><a class="markdown-anchor" href="#eggstartcluster">#</a> <code>egg.startCluster</code></h4>
<p>Egg 的多进程启动器，由这个方法来启动 Master，主要的功能实现在 <a href="https://github.com/eggjs/egg-cluster" target="_blank" rel="noopener">egg-cluster</a> 上。所以直接使用 EggCore 还是单进程的方式，而 Egg 实现了多进程。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> startCluster = <span class="built_in">require</span>(<span class="string">'egg'</span>).startCluster;</span><br><span class="line">startCluster(&#123;</span><br><span class="line">  <span class="comment">// 应用的代码目录</span></span><br><span class="line">  baseDir: <span class="string">'/path/to/app'</span>,</span><br><span class="line">  <span class="comment">// 需要通过这个参数来指定框架目录</span></span><br><span class="line">  framework: <span class="string">'/path/to/framework'</span>,</span><br><span class="line">&#125;, () =&gt; &#123;</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">'app started'</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>所有参数可以查看 <a href="https://github.com/eggjs/egg-cluster#options" target="_blank" rel="noopener">egg-cluster</a></p>
<h4 id="eggapplication-和-eggagent"><a class="markdown-anchor" href="#eggapplication-和-eggagent">#</a> <code>egg.Application</code> 和 <code>egg.Agent</code></h4>
<p>进程中的唯一单例，但 Application 和 Agent 存在一定差异。如果框架继承于 Egg，会定制这两个类，那 framework 应该 export 这两个类。</p>
<h4 id="eggappworkerloader-和-eggagentworkerloader"><a class="markdown-anchor" href="#eggappworkerloader-和-eggagentworkerloader">#</a> <code>egg.AppWorkerLoader</code> 和 <code>egg.AgentWorkerLoader</code></h4>
<p>框架也存在定制 Loader 的场景，覆盖原方法或者新加载目录都需要提供自己的 Loader，而且必须要继承 Egg 的 Loader。</p>
<h3 id="框架继承"><a class="markdown-anchor" href="#框架继承">#</a> 框架继承</h3>
<p>框架支持继承关系，可以把框架比作一个类，那么基类就是 Egg 框架，如果想对 Egg 做扩展就继承。</p>
<p>首先定义一个框架需要实现 Egg 所有的 API</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// package.json</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="string">"name"</span>: <span class="string">"yadan"</span>,</span><br><span class="line">  <span class="string">"dependencies"</span>: &#123;</span><br><span class="line">    <span class="string">"egg"</span>: <span class="string">"^2.0.0"</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// index.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="built_in">require</span>(<span class="string">'./lib/framework.js'</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// lib/framework.js</span></span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">'path'</span>);</span><br><span class="line"><span class="keyword">const</span> egg = <span class="built_in">require</span>(<span class="string">'egg'</span>);</span><br><span class="line"><span class="keyword">const</span> EGG_PATH = <span class="built_in">Symbol</span>.for(<span class="string">'egg#eggPath'</span>);</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Application</span> <span class="keyword">extends</span> <span class="title">egg</span>.<span class="title">Application</span> </span>&#123;</span><br><span class="line">  get [EGG_PATH]() &#123;</span><br><span class="line">    <span class="comment">// 返回 framework 路径</span></span><br><span class="line">    <span class="keyword">return</span> path.dirname(__dirname);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 覆盖了 Egg 的 Application</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="built_in">Object</span>.assign(egg, &#123;</span><br><span class="line">  Application,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>应用启动时需要指定框架名（在 <code>package.json</code> 指定 <code>egg.framework</code>，默认为 egg），Loader 将从 <code>node_modules</code> 找指定模块作为框架，并加载其 export 的 Application。</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"scripts"</span>: &#123;</span><br><span class="line">    <span class="attr">"dev"</span>: <span class="string">"egg-bin dev"</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">"egg"</span>: &#123;</span><br><span class="line">    <span class="attr">"framework"</span>: <span class="string">"yadan"</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>现在 yadan 框架目录已经是一个 loadUnit，那么相应目录和文件（如 <code>app</code> 和 <code>config</code>）都会被加载，查看<a href="./loader.html">框架被加载的文件</a>。</p>
<h3 id="框架继承原理"><a class="markdown-anchor" href="#框架继承原理">#</a> 框架继承原理</h3>
<p>使用 <code>Symbol.for('egg#eggPath')</code> 来指定当前框架的路径，目的是让 Loader 能探测到框架的路径。为什么这样实现呢？其实最简单的方式是将框架的路径传递给 Loader，但我们需要实现多级框架继承，每一层框架都要提供自己的当前路径，并且需要继承存在先后顺序。</p>
<p>现在的实现方案是基于类继承的，每一层框架都必须继承上一层框架并且指定 eggPath，然后遍历原型链就能获取每一层的框架路径了。</p>
<p>比如有三层框架：部门框架（department）&gt; 企业框架（enterprise）&gt; Egg</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// enterprise</span></span><br><span class="line"><span class="keyword">const</span> Application = <span class="built_in">require</span>(<span class="string">'egg'</span>).Application;</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Enterprise</span> <span class="keyword">extends</span> <span class="title">Application</span> </span>&#123;</span><br><span class="line">  get [EGG_PATH]() &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">'/path/to/enterprise'</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 自定义模块 Application</span></span><br><span class="line">exports.Application = Enterprise;</span><br><span class="line"></span><br><span class="line"><span class="comment">// department</span></span><br><span class="line"><span class="keyword">const</span> Application = <span class="built_in">require</span>(<span class="string">'enterprise'</span>).Application;</span><br><span class="line"><span class="comment">// 继承 enterprise 的 Application</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">department</span> <span class="keyword">extends</span> <span class="title">Application</span> </span>&#123;</span><br><span class="line">  get [EGG_PATH]() &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">'/path/to/department'</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 启动需要传入 department 的框架路径才能获取 Application</span></span><br><span class="line"><span class="keyword">const</span> Application = <span class="built_in">require</span>(<span class="string">'department'</span>).Application;</span><br><span class="line"><span class="keyword">const</span> app = <span class="keyword">new</span> Application();</span><br><span class="line">app.ready();</span><br></pre></td></tr></table></figure>
<p>以上均是伪代码，为了详细说明框架路径的加载过程，不过 Egg 已经在<a href="../core/development.html">本地开发</a>和<a href="../core/deployment.html">应用部署</a>提供了很好的工具，不需要自己实现。</p>
<h3 id="自定义-agent"><a class="markdown-anchor" href="#自定义-agent">#</a> 自定义 Agent</h3>
<p>上面的例子自定义了 Application，因为 Egg 是多进程模型，所以还需要定义 Agent，原理是一样的。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// lib/framework.js</span></span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">'path'</span>);</span><br><span class="line"><span class="keyword">const</span> egg = <span class="built_in">require</span>(<span class="string">'egg'</span>);</span><br><span class="line"><span class="keyword">const</span> EGG_PATH = <span class="built_in">Symbol</span>.for(<span class="string">'egg#eggPath'</span>);</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Application</span> <span class="keyword">extends</span> <span class="title">egg</span>.<span class="title">Application</span> </span>&#123;</span><br><span class="line">  get [EGG_PATH]() &#123;</span><br><span class="line">    <span class="comment">// 返回 framework 路径</span></span><br><span class="line">    <span class="keyword">return</span> path.dirname(__dirname);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Agent</span> <span class="keyword">extends</span> <span class="title">egg</span>.<span class="title">Agent</span> </span>&#123;</span><br><span class="line">  get [EGG_PATH]() &#123;</span><br><span class="line">    <span class="keyword">return</span> path.dirname(__dirname);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 覆盖了 Egg 的 Application</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="built_in">Object</span>.assign(egg, &#123;</span><br><span class="line">  Application,</span><br><span class="line">  Agent,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p><strong>但因为 Agent 和 Application 是两个实例，所以 API 有可能不一致。</strong></p>
<h3 id="自定义-loader"><a class="markdown-anchor" href="#自定义-loader">#</a> 自定义 Loader</h3>
<p>Loader 应用启动的核心，使用它还能规范应用代码，我们可以基于这个类扩展更多功能，比如加载数据代码。扩展 Loader 还能覆盖默认的实现，或调整现有的加载顺序等。</p>
<p>自定义 Loader 也是用 <code>Symbol.for('egg#loader')</code> 的方式，主要的原因还是使用原型链，上层框架可覆盖底层 Loader，在上面例子的基础上</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// lib/framework.js</span></span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">'path'</span>);</span><br><span class="line"><span class="keyword">const</span> egg = <span class="built_in">require</span>(<span class="string">'egg'</span>);</span><br><span class="line"><span class="keyword">const</span> EGG_PATH = <span class="built_in">Symbol</span>.for(<span class="string">'egg#eggPath'</span>);</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">YadanAppWorkerLoader</span> <span class="keyword">extends</span> <span class="title">egg</span>.<span class="title">AppWorkerLoader</span> </span>&#123;</span><br><span class="line">  load() &#123;</span><br><span class="line">    <span class="keyword">super</span>.load();</span><br><span class="line">    <span class="comment">// 自己扩展</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Application</span> <span class="keyword">extends</span> <span class="title">egg</span>.<span class="title">Application</span> </span>&#123;</span><br><span class="line">  get [EGG_PATH]() &#123;</span><br><span class="line">    <span class="comment">// 返回 framework 路径</span></span><br><span class="line">    <span class="keyword">return</span> path.dirname(__dirname);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 覆盖 Egg 的 Loader，启动时使用这个 Loader</span></span><br><span class="line">  get [EGG_LOADER]() &#123;</span><br><span class="line">    <span class="keyword">return</span> YadanAppWorkerLoader;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 覆盖了 Egg 的 Application</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="built_in">Object</span>.assign(egg, &#123;</span><br><span class="line">  Application,</span><br><span class="line">  <span class="comment">// 自定义的 Loader 也需要 export，上层框架需要基于这个扩展</span></span><br><span class="line">  AppWorkerLoader: YadanAppWorkerLoader,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>AgentWorkerLoader 扩展也类似，这里不再举例。AgentWorkerLoader 加载的文件可以于 AppWorkerLoader 不同，比如：默认加载时，Egg 的 AppWorkerLoader 会加载 <code>app.js</code> 而 AgentWorkerLoader 加载的是 <code>agent.js</code>。</p>
<h2 id="框架启动原理"><a class="markdown-anchor" href="#框架启动原理">#</a> 框架启动原理</h2>
<p>框架启动在<a href="../core/cluster-and-ipc.html">多进程模型</a>、<a href="./loader.html">Loader</a>、<a href="./plugin.html">插件</a>中或多或少都提过，这里系统的梳理下启动顺序。</p>
<ul>
<li>startCluster 启动传入 <code>baseDir</code> 和 <code>framework</code>，Master 进程启动</li>
<li>Master 先 fork Agent Worker
<ul>
<li>根据 framework 找到框架目录，实例化该框架的 Agent 类</li>
<li>Agent 找到定义的 AgentWorkerLoader，开始进行加载</li>
<li>AgentWorkerLoader，开始进行加载 整个加载过程是同步的，按 plugin &gt; config &gt; extend &gt; <code>agent.js</code> &gt; 其他文件顺序加载</li>
<li><code>agent.js</code> 可自定义初始化，支持异步启动，如果定义了 beforeStart 会等待执行完成之后通知 Master 启动完成。</li>
</ul>
</li>
<li>Master 得到 Agent Worker 启动成功的消息，使用 cluster fork App Worker
<ul>
<li>App Worker 有多个进程，所以这几个进程是并行启动的，但执行逻辑是一致的</li>
<li>单个 App Worker 和 Agent 类似，通过 framework 找到框架目录，实例化该框架的 Application 类</li>
<li>Application 找到 AppWorkerLoader，开始进行加载，顺序也是类似的，会异步等待，完成后通知 Master 启动完成</li>
</ul>
</li>
<li>Master 等待多个 App Worker 的成功消息后启动完成，能对外提供服务。</li>
</ul>
<h2 id="框架测试"><a class="markdown-anchor" href="#框架测试">#</a> 框架测试</h2>
<p>在看下文之前请先查看<a href="../core/unittest.html">单元测试章节</a>，框架测试的大部分使用场景和应用类似。</p>
<h3 id="初始化"><a class="markdown-anchor" href="#初始化">#</a> 初始化</h3>
<p>框架的初始化方式有一定差异</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> mock = <span class="built_in">require</span>(<span class="string">'egg-mock'</span>);</span><br><span class="line">describe(<span class="string">'test/index.test.js'</span>, () =&gt; &#123;</span><br><span class="line">  <span class="keyword">let</span> app;</span><br><span class="line">  before(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">    app = mock.app(&#123;</span><br><span class="line">      <span class="comment">// 转换成 test/fixtures/apps/example</span></span><br><span class="line">      baseDir: <span class="string">'apps/example'</span>,</span><br><span class="line">      <span class="comment">// 重要：配置 framework</span></span><br><span class="line">      framework: <span class="literal">true</span>,</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">return</span> app.ready();</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  after(<span class="function"><span class="params">()</span> =&gt;</span> app.close());</span><br><span class="line">  afterEach(mock.restore);</span><br><span class="line"></span><br><span class="line">  it(<span class="string">'should success'</span>, () =&gt; &#123;</span><br><span class="line">    <span class="keyword">return</span> app.httpRequest()</span><br><span class="line">      .get(<span class="string">'/'</span>)</span><br><span class="line">      .expect(<span class="number">200</span>);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<ul>
<li>框架和应用不同，应用测试当前代码，而框架是测试框架代码，所以会频繁更换 baseDir 达到测试各种应用的目的。</li>
<li>baseDir 有潜规则，我们一般会把测试的应用代码放到 <code>test/fixtures</code> 下，所以自动补全，也可以传入绝对路径。</li>
<li>必须指定 <code>framework: true</code>，告知当前路径为框架路径，也可以传入绝对路径。</li>
<li>app 应用需要在 before 等待 ready，不然在 testcase 里无法获取部分 API</li>
<li>框架在测试完毕后需要使用 <code>app.close()</code> 关闭，不然会有遗留问题，比如日志写文件未关闭导致 fd 不够。</li>
</ul>
<h3 id="缓存"><a class="markdown-anchor" href="#缓存">#</a> 缓存</h3>
<p>在测试多环境场景需要使用到 cache 参数，因为 <code>mm.app</code> 默认有缓存，当第一次加载过后再次加载会直接读取缓存，那么设置的环境也不会生效。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> mock = <span class="built_in">require</span>(<span class="string">'egg-mock'</span>);</span><br><span class="line">describe(<span class="string">'/test/index.test.js'</span>, () =&gt; &#123;</span><br><span class="line">  <span class="keyword">let</span> app;</span><br><span class="line">  afterEach(<span class="function"><span class="params">()</span> =&gt;</span> app.close());</span><br><span class="line"></span><br><span class="line">  it(<span class="string">'should test on local'</span>, () =&gt; &#123;</span><br><span class="line">    mock.env(<span class="string">'local'</span>);</span><br><span class="line">    app = mock.app(&#123;</span><br><span class="line">      baseDir: <span class="string">'apps/example'</span>,</span><br><span class="line">      framework: <span class="literal">true</span>,</span><br><span class="line">      cache: <span class="literal">false</span>,</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">return</span> app.ready();</span><br><span class="line">  &#125;);</span><br><span class="line">  it(<span class="string">'should test on prod'</span>, () =&gt; &#123;</span><br><span class="line">    mock.env(<span class="string">'prod'</span>);</span><br><span class="line">    app = mock.app(&#123;</span><br><span class="line">      baseDir: <span class="string">'apps/example'</span>,</span><br><span class="line">      framework: <span class="literal">true</span>,</span><br><span class="line">      cache: <span class="literal">false</span>,</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">return</span> app.ready();</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<h3 id="多进程测试"><a class="markdown-anchor" href="#多进程测试">#</a> 多进程测试</h3>
<p>很少场景会使用多进程测试，因为多进程无法进行 API 级别的 mock 导致测试成本很高，而进程在有覆盖率的场景启动很慢，测试会超时。但多进程测试是验证多进程模型最好的方式，还可以测试 stdout 和 stderr。</p>
<p>多进程测试和 <code>mm.app</code> 参数一致，但 app 的 API 完全不同，不过 SuperTest 依然可用。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> mock = <span class="built_in">require</span>(<span class="string">'egg-mock'</span>);</span><br><span class="line">describe(<span class="string">'/test/index.test.js'</span>, () =&gt; &#123;</span><br><span class="line">  <span class="keyword">let</span> app;</span><br><span class="line">  before(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">    app = mock.cluster(&#123;</span><br><span class="line">      baseDir: <span class="string">'apps/example'</span>,</span><br><span class="line">      framework: <span class="literal">true</span>,</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">return</span> app.ready();</span><br><span class="line">  &#125;);</span><br><span class="line">  after(<span class="function"><span class="params">()</span> =&gt;</span> app.close());</span><br><span class="line">  afterEach(mock.restore);</span><br><span class="line">  it(<span class="string">'should success'</span>, () =&gt; &#123;</span><br><span class="line">    <span class="keyword">return</span> app.httpRequest()</span><br><span class="line">      .get(<span class="string">'/'</span>)</span><br><span class="line">      .expect(<span class="number">200</span>);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>多进程测试还可以测试 stdout/stderr，因为 <code>mm.cluster</code> 是基于 <a href="https://github.com/popomore/coffee" target="_blank" rel="noopener">coffee</a> 扩展的，可进行进程测试。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> mock = <span class="built_in">require</span>(<span class="string">'egg-mock'</span>);</span><br><span class="line">describe(<span class="string">'/test/index.test.js'</span>, () =&gt; &#123;</span><br><span class="line">  <span class="keyword">let</span> app;</span><br><span class="line">  before(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">    app = mock.cluster(&#123;</span><br><span class="line">      baseDir: <span class="string">'apps/example'</span>,</span><br><span class="line">      framework: <span class="literal">true</span>,</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">return</span> app.ready();</span><br><span class="line">  &#125;);</span><br><span class="line">  after(<span class="function"><span class="params">()</span> =&gt;</span> app.close());</span><br><span class="line">  it(<span class="string">'should get `started`'</span>, () =&gt; &#123;</span><br><span class="line">    <span class="comment">// 判断终端输出</span></span><br><span class="line">    app.expect(<span class="string">'stdout'</span>, /started/);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

  </article>
  <aside id="mobileAside" class="toc">
  <div class="mobile-menu">
    <ul>
      <li><a href="/zh-cn/intro/" alt="指南">指南</a></li><li><a href="/api/" alt="API">API</a></li><li><a href="/zh-cn/tutorials/index.html" alt="教程">教程</a></li><li><a href="https://github.com/search?q=topic%3Aegg-plugin&type=Repositories" alt="插件">插件</a></li><li><a href="https://github.com/eggjs/egg/releases" alt="发布日志">发布日志</a></li>
      
      
        <li class="translations">
          <a class="nav-link">Translations</a>
          <span class="arrow"></span><ul id="dropdownContent" class="dropdown-content"><li><a id="en" href="/en/advanced/framework.html" >English</a></li><li><a id="zh-cn" href="/zh-cn/advanced/framework.html" style="color: #22ab28">中文</a></li></ul>
        </li>
      
    </ul>
  </div>
  <dl><dt id="title-Intro" style="cursor: pointer;" class="aside-title">新手指南<a id="collapse-icon-Intro" class="icon opend"></a></dt><dd id=panel-Intro><ul><li><a href="/zh-cn/intro/index.html" class="menu-link">Egg.js 是什么?</a></li><li><a href="/zh-cn/intro/egg-and-koa.html" class="menu-link">Egg.js 和 Koa</a></li><li><a href="/zh-cn/intro/quickstart.html" class="menu-link">快速入门</a></li><li><a href="/zh-cn/tutorials/progressive.html" class="menu-link">渐进式开发</a></li><li><a href="/zh-cn/migration.html" class="menu-link">2.x 升级指南</a></li></ul></dd><dt id="title-Basics" style="cursor: pointer;" class="aside-title">基础功能<a id="collapse-icon-Basics" class="icon opend"></a></dt><dd id=panel-Basics><ul><li><a href="/zh-cn/basics/structure.html" class="menu-link">目录结构</a></li><li><a href="/zh-cn/basics/objects.html" class="menu-link">内置对象</a></li><li><a href="/zh-cn/basics/env.html" class="menu-link">运行环境</a></li><li><a href="/zh-cn/basics/config.html" class="menu-link">配置</a></li><li><a href="/zh-cn/basics/middleware.html" class="menu-link">中间件</a></li><li><a href="/zh-cn/basics/router.html" class="menu-link">Router</a></li><li><a href="/zh-cn/basics/controller.html" class="menu-link">Controller</a></li><li><a href="/zh-cn/basics/service.html" class="menu-link">Service</a></li><li><a href="/zh-cn/basics/plugin.html" class="menu-link">插件</a></li><li><a href="/zh-cn/basics/schedule.html" class="menu-link">定时任务</a></li><li><a href="/zh-cn/basics/extend.html" class="menu-link">框架扩展</a></li><li><a href="/zh-cn/basics/app-start.html" class="menu-link">启动自定义</a></li></ul></dd><dt id="title-Core" style="cursor: pointer;" class="aside-title">核心功能<a id="collapse-icon-Core" class="icon opend"></a></dt><dd id=panel-Core><ul><li><a href="/zh-cn/core/development.html" class="menu-link">本地开发</a></li><li><a href="/zh-cn/core/unittest.html" class="menu-link">单元测试</a></li><li><a href="/zh-cn/core/deployment.html" class="menu-link">应用部署</a></li><li><a href="/zh-cn/core/logger.html" class="menu-link">日志</a></li><li><a href="/zh-cn/core/httpclient.html" class="menu-link">HttpClient</a></li><li><a href="/zh-cn/core/cookie-and-session.html" class="menu-link">Cookie and Session</a></li><li><a href="/zh-cn/core/cluster-and-ipc.html" class="menu-link">多进程模型和进程间通讯</a></li><li><a href="/zh-cn/core/view.html" class="menu-link">模板渲染</a></li><li><a href="/zh-cn/core/error-handling.html" class="menu-link">异常处理</a></li><li><a href="/zh-cn/core/security.html" class="menu-link">安全</a></li><li><a href="/zh-cn/core/i18n.html" class="menu-link">国际化</a></li></ul></dd><dt id="title-Tutorials" style="cursor: pointer;" class="aside-title">教程<a id="collapse-icon-Tutorials" class="icon opend"></a></dt><dd id=panel-Tutorials><ul><li><a href="/zh-cn/tutorials/mysql.html" class="menu-link">MySQL</a></li><li><a href="/zh-cn/tutorials/restful.html" class="menu-link">RESTful API</a></li><li><a href="/zh-cn/tutorials/passport.html" class="menu-link">Passport 鉴权</a></li><li><a href="/zh-cn/tutorials/socketio.html" class="menu-link">Socket.IO</a></li><li><a href="/zh-cn/tutorials/assets.html" class="menu-link">静态资源</a></li><li><a href="/zh-cn/tutorials/typescript.html" class="menu-link">TypeScript</a></li></ul></dd><dt id="title-Advanced" style="cursor: pointer;" class="aside-title">进阶<a id="collapse-icon-Advanced" class="icon opend"></a></dt><dd id=panel-Advanced><ul><li><a href="/zh-cn/advanced/loader.html" class="menu-link">Loader</a></li><li><a href="/zh-cn/advanced/plugin.html" class="menu-link">插件开发</a></li><li><a href="/zh-cn/advanced/framework.html" class="menu-link">框架开发</a></li><li><a href="/zh-cn/advanced/cluster-client.html" class="menu-link">多进程研发模式增强</a></li><li><a href="/zh-cn/advanced/view-plugin.html" class="menu-link">模板插件开发规范</a></li><li><a href="/zh-cn/style-guide.html" class="menu-link">代码风格指南</a></li></ul></dd><dt id="title-Community" style="cursor: pointer;" class="aside-title">社区<a id="collapse-icon-Community" class="icon opend"></a></dt><dd id=panel-Community><ul><li><a href="/zh-cn/plugins/" class="menu-link">内置插件列表</a></li><li><a href="/zh-cn/contributing.html" class="menu-link">如何贡献</a></li><li><a href="/zh-cn/resource.html" class="menu-link">资源</a></li><li><a href="/zh-cn/faq.html" class="menu-link">常见问题</a></li></ul></dd></dl>
</aside>
<script>
var mobileTrigger = document.getElementById('mobileTrigger');
var mobileAside = document.getElementById('mobileAside');

var expandMenu = function(title) {
  // handle icon
  const collapseIcon = document.getElementById('collapse-icon-' + title);
  if (collapseIcon) {
    collapseIcon.className = 'icon opend';
  }
  // handle panelEle
  const panelEle = document.getElementById('panel-' + title);
  if (panelEle) {
    panelEle.className = '';
  }
}

var collapseMenu = function(title) {
  // handle icon
  const collapseIcon = document.getElementById('collapse-icon-' + title);
  if (collapseIcon) {
    collapseIcon.className = 'icon closed';
  }
  // handle panelEle
  const panelEle = document.getElementById('panel-' + title);
  if (panelEle) {
    panelEle.className = 'aside-panel-hidden';
  }
}

mobileAside.onclick = function(e) {
  const targetId = e.target.id;
  if (targetId && (targetId.indexOf('title-') > -1 || targetId.indexOf('collapse-icon-') > -1)) {
    const title = targetId.replace('title-', '').replace('collapse-icon-', '');
    try { 
      // the the browser may have no localStroage or JSON.parse may throw exception.
      const menuInfo = JSON.parse(window.localStorage.getItem('menuInfo'));
        
      // current menu status
      const curClosed = menuInfo[title] ? menuInfo[title].closed : false; // default false

      // change UI
      curClosed ? expandMenu(title) : collapseMenu(title);

      // save menuInfo to localStorage
      menuInfo[title] = { closed: !curClosed } // opposite
      window.localStorage.setItem('menuInfo', JSON.stringify(menuInfo));
    } catch (e) {}
  }
};

mobileTrigger.onclick = function(e) {
  e.preventDefault();
  if (mobileAside.className.indexOf('mobile-show') === -1) {
    mobileAside.className += ' mobile-show';
  } else {
    mobileAside.className = 'toc';
  }
};

(function() {
  // save data to localStorage because the page will refresh when user change the url.
  let menuInfo;
  try { 
    // the the browser may have no localStroage or JSON.parse may throw exception.
    menuInfo = JSON.parse(window.localStorage.getItem('menuInfo'));
    if (!menuInfo) {
      menuInfo = {};
      window.localStorage.setItem('menuInfo', JSON.stringify(menuInfo));
    }
  } catch (e) {
    menuInfo = {}; // default {}
  }

  for (const title in menuInfo) {
    if (menuInfo[title] && menuInfo[title].closed) { // menu in closed status.
      collapseMenu(title);
    } else {
      expandMenu(title);
    }
  }

  // highlight menu
  const pathname = window.location.pathname;
  const selector = `a[href="${pathname}"].menu-link,a[href="${pathname}index.html"].menu-link`;
  const menuItem = mobileAside.querySelector(selector);
  if (menuItem) { menuItem.className += ' highlight'; }
})();
</script>

</div>

  </div>
</body>
<script src="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.js"></script>
<script>
docsearch({
  apiKey: '1561de31a86f79507ea00cdb54ce647c',
  indexName: 'eggjs',
  inputSelector: '#search-query',
});
</script>
<div class="cnzz">
<script src="https://s11.cnzz.com/z_stat.php?id=1261142226&web_id=1261142226" language="JavaScript"></script>
</div>

</html>
